package com.mapforge.util;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * A BitOutputStream is a buffered stream designed to write an arbitrary (up to
 * a limit of 32) number of bits. The data (the bits) is first written to an
 * internal bit buffer and then copied into an internal byte buffer if the bit
 * buffer is full. The data in the byte buffer is written to the underlying
 * stream whenever the byte buffer reaches its capacity, the BitOutputStream is
 * closed, or is explicitly flushed. It should be noticed that BitInputStream is
 * not thread-safe. None of the methods are synchronized.
 * 
 * @author Ulf Uttersrud, Oslo University College
 * @version 2.0 17.05.2008
 */

public class BitOutputStream {
	private static final int DEFAULT_BUFFER_SIZE = 4096;

	/**
	 * The internal byte array where the bytes are temporarily stored.
	 */
	private byte[] buf;

	/**
	 * The size of the internal byte array.
	 */
	private int bufSize;

	/**
	 * The current position in the byte buffer. That is the position in the byte
	 * array buf where the next byte will be written. The value is always in the
	 * range 0 through buf.length and elements buf[0] through buf[pos-1] contain
	 * valid bytes. The byte array is full, and hence has to be emptied, if pos
	 * >= buf.length.
	 */
	private int pos;

	/**
	 * An int used as an internal bit buffer.
	 */
	private int bits;

	/**
	 * The number of valid bits (the rightmost bits) in the bit buffer.
	 */
	private int bitSize;

	/**
	 * The underlying output stream.
	 */
	private OutputStream out;

	/**
	 * Creates a BitOutputStream with the specified size as the length of the
	 * internal byte array and saves its other argument, the output stream, for
	 * later use.
	 * 
	 * @param out
	 *            the underlying output stream.
	 * @param size
	 *            the length of the byte buffer.
	 * @exception IllegalArgumentException
	 *                if size <= 0.
	 * @exception NullPointerException
	 *                if the output stream is not defined.
	 */
	public BitOutputStream(OutputStream out, int size) {
		if (out == null)
			throw new NullPointerException("The stream out is null");

		if (size <= 0)
			throw new IllegalArgumentException("size <= 0");

		buf = new byte[bufSize = size];
		this.out = out;
	}

	/**
	 * Creates a BitOutputStream and saves its argument, the output stream, for
	 * later use. A default value of 512 is used as a buffer length.
	 * 
	 * @param out
	 *            the underlying output stream.
	 * @exception NullPointerException
	 *                if the the output stream is not defined.
	 */
	public BitOutputStream(OutputStream out) {
		this(out, DEFAULT_BUFFER_SIZE);
	}

	/**
	 * Creates a BitOutputStream to a file, with the filename as argument. A
	 * default value DEFAULT_BUFFER_SIZE is used as the length of the internal
	 * byte array.
	 * 
	 * @param fileName
	 *            the name of the file.
	 * @exception FileNotFoundException
	 *                if the file does not exist.
	 */
	public BitOutputStream(String fileName) throws FileNotFoundException {
		this(new FileOutputStream(fileName), DEFAULT_BUFFER_SIZE);
	}

	/**
	 * Creates a BitOutputStream to a file, with the filename as argument and
	 * with size as the length of the internal byte array.
	 * 
	 * @param fileName
	 *            the name of the file.
	 * @exception FileNotFoundException
	 *                if the file does not exist.
	 * @exception IllegalArgumentException
	 *                if size <= 0.
	 */
	public BitOutputStream(String fileName, int size)
			throws FileNotFoundException {
		this(new FileOutputStream(fileName), size);
	}

	/**
	 * Creates a BitOutputStream to a file with the argument as filename. A
	 * default value is used as a buffer length.
	 * 
	 * @param fileName
	 *            the name of the file.
	 * @exception FileNotFoundException
	 *                if the file can not be created.
	 */
	public static BitOutputStream toFile(String fileName)
			throws FileNotFoundException {
		return new BitOutputStream(new FileOutputStream(fileName));
	}

	/**
	 * Creates a BitOutputStream to a file with the specified filename. If the
	 * second argument append is true, then bits will be written to the end of
	 * the file rather than the beginning. A default value is used as a buffer
	 * length.
	 * 
	 * @param fileName
	 *            the name of the file.
	 * @param append
	 *            if true, then bits will be written to the end of the file
	 *            rather than the beginning.
	 * @exception FileNotFoundException
	 *                if the file cannot be created or opened.
	 */
	public static BitOutputStream toFile(String fileName, boolean append)
			throws FileNotFoundException {
		return new BitOutputStream(new FileOutputStream(fileName, append));
	}

	/**
	 * Flushes the internal buffer
	 */
	private void flushBuffer() throws IOException {
		if (out == null)
			throw new IOException("The stream is closed!");

		if (pos > 0) {
			out.write(buf, 0, pos);
			pos = 0;
		}
	} // end flushBuffer

	/**
	 * Writes one bit (i.e. the rightmost bit of the argument bit) to the output
	 * stream.
	 * 
	 * @param bit
	 *            containing the bit
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void writeBit(int bit) throws IOException {
		bits <<= 1; // makes room for a new bit
		bits |= (bit & 1); // the last bit of the parameter bit is added
		bitSize++; // bitSize is updated

		if (bitSize >= 8) // a byte can be moved to the byte buffer
		{
			bitSize = 0;
			if (pos >= bufSize) // is the byte buffer full?
				flushBuffer();
			buf[pos++] = (byte) bits; // a byte is moved
		}
	} // end writeBit

	/**
	 * Writes a 0-bit to the output stream.
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void write0Bit() throws IOException {
		bits <<= 1; // adds a 0-bit
		bitSize++; // bitSize is updated

		if (bitSize >= 8) // 
		{
			bitSize = 0;
			if (pos >= bufSize) // is the byte buffer full?
				flushBuffer();
			buf[pos++] = (byte) bits; // a byte is moved
		}
	} // end write0Bit

	/**
	 * Writes a 1-bit to the output stream.
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void write1Bit() throws IOException {
		bits <<= 1; // makes room for a new bit
		bits |= 1; // adds a 1-bit
		bitSize++; // bitSize is updated

		if (bitSize >= 8) // a byte can be moved to the byte buffer
		{
			bitSize = 0;
			if (pos >= bufSize) // is the byte buffer full?
				flushBuffer();
			buf[pos++] = (byte) bits; // a byte is moved
		}
	} // end write1Bit

	/**
	 * Writes 8 bits (a byte) (i.e. the rightmost 8 bits of the argument b) to
	 * the output stream.
	 * 
	 * @param b
	 *            containing the byte
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void write(int b) throws IOException {
		bits <<= 8; // makes room for 8 new bits
		bits |= (b & 0xff); // adds 8 bits

		if (pos >= bufSize) // is the byte buffer full?
			flushBuffer();
		buf[pos++] = (byte) (bits >> bitSize); // a byte is moved

	} // end writeBit

	/**
	 * Writes the requested (rightmost) numberOfBits from the argument value to
	 * the output stream. An IllegalArgumentException is thrown if the requested
	 * number of bits is outside the interval [0,32].
	 * 
	 * @param value
	 *            containing the bits
	 * @param numberOfBits
	 *            the (rightmost) number of bits from value to be written
	 * @exception IOException
	 *                if an I/O error occurs.
	 * @exception IllegalArgumentException
	 *                if numberOfBits is outside the interval [0,32]
	 */
	public void writeBits(int value, int numberOfBits) throws IOException {
		if (numberOfBits < 0)
			throw new IllegalArgumentException("Cannot write " + numberOfBits
					+ " bits!");

		if (numberOfBits <= 25) // the most common case
		{
			bits <<= numberOfBits; // will not create overflow

			bits |= (value & ((1 << numberOfBits) - 1)); // bits are added
			bitSize += numberOfBits; // bitSize is updated

			while (bitSize >= 8) {
				bitSize -= 8;
				if (pos >= bufSize) // is the byte buffer full?
					flushBuffer();
				buf[pos++] = (byte) (bits >> bitSize); // a byte is moved
			}
		} else if (numberOfBits <= 32) {
			// 25 bits are written in one operation and then
			// the rest of the bits (numberOfBits - 25) are
			// written in a second operation

			int k = numberOfBits - 25;
			bits <<= 25; // makes room for 25 bits - does not create overflow
			bits |= (value >> k) & 0x1ffffff; // 25 bits are added
			bitSize += 25; // bitSize is updated

			while (bitSize >= 8) {
				bitSize -= 8;
				if (pos >= bufSize) // is the byte buffer full?
					flushBuffer();
				buf[pos++] = (byte) (bits >> bitSize); // a byte is moved
			}

			bits <<= k; // k = numberOfBits - 25
			bits |= (value & ((1 << k) - 1)); // k bits are added
			bitSize += k; // bitSize is updated

			if (bitSize >= 8) {
				bitSize -= 8;
				if (pos >= bufSize) // is the byte buffer full?
					flushBuffer();
				buf[pos++] = (byte) (bits >> bitSize); // a byte is moved
			}
		} else
			// numberOfBits > 32
			throw new IllegalArgumentException("Cannot write " + numberOfBits
					+ " bits!");

	} // end writeBits

	/**
	 * Flushes this buffered output stream. This forces any buffered output bits
	 * to be written out to the underlying output stream. If the number of
	 * buffered output bits is not a multiple of 8, then a sufficient number of
	 * 0-bits are added.
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void flush() throws IOException {
		if (bitSize > 0) {
			// the byte buffer is written if it is full
			if (pos >= bufSize)
				flushBuffer();

			// 0-bits are added to create a full byte
			buf[pos++] = (byte) (bits <<= (8 - bitSize));
			bitSize = 0;
		}

		flushBuffer();
		out.flush();

	} // end flush()

	/**
	 * Returns the number of bits missing in order to fill the last byte. If the
	 * method is called previous to close (or flush), the return value tells how
	 * many 0-bits are shifted into the last byte.
	 * 
	 * @return the number of bits missing in order to fill the last byte.
	 */
	public int missingBits() throws IOException {
		if (out == null)
			throw new IOException("The stream is closed!");
		return bitSize == 0 ? 0 : 8 - bitSize;
	}

	/**
	 * Ensures that the close method of this bit output stream is called when
	 * there are no more references to it.
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	protected void finalize() throws IOException {
		if (out != null)
			close();
	}

	/**
	 * Closes the underlying output stream and releases any system resources
	 * associated with the stream. The internal buffers are emptied (the method
	 * flush() is called) before the underlying stream is closed. Any method
	 * call subsequent to a close will result in an IOException with the message
	 * "The stream is closed!".
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void close() throws IOException {
		// a second call to close will have no effect
		if (out == null)
			return;
		flush();
		out.close();

		// The variables out and buf are set to null,
		// pos to bufSize and bitSize to 8. This
		// will prevent any method call subsequent to a close.
		// Such an effort will result in an IOException with
		// the message "The stream is closed!".
		out = null;
		buf = null;
		pos = bufSize = -1;
		bitSize = 8;

	} // end close()

} // end class BitOutputStream
